7주차 - 모드, 노드
개요
-
-
- 커스텀 자원 직접 만든 후에, 이걸로 커스텀 스케줄링 연습해보자.
- 각 노드별로 8개의 자원을 넣는다.
- 이후에 mostallocated쓴다.
- 카펜터를 따라할 순 없을까?
-
-
-
-
커스텀 스케줄러 만들기
스케줄링 기법들을 사용하는 건 전략을 어떻게 세우냐에 달렸다.
그런데 간혹 이것만으로는 부족할 때가 있다.
이럴 때는 결국 커스텀 스케줄러를 만드는 수밖에 없다.
커스텀 스케줄러를 사용함으로써 얻는 이점 중 가장 큰 것은 말 그대로 스케줄링을 커스텀할 수 있다는 것이다.
그러나 또다른 이점이 있는데, 바로 스케줄러의 부하를 줄일 수 있다는 것이다.
기본적으로 스케줄러를 HA한다고 해도 이들은 리더를 두는 방식으로 운용되기 때문에 결국 하나의 스케줄러가 부하를 감당한다.
이럴 때는 아예 스케줄러를 새로 두고, 이를 감당하는 프로세스를 별도로 분리하는 방식으로 부하를 줄이는 전략을 사용할 수 있다.
한번 커스텀 스케줄러를 만들어보자.
먼저 기본스케줄러의 정보를 뜯어본다.
크게 알아야 할 것은 스케줄러가 클러스터에서 어떤 신원으로 활동하는지, 어떻게 설정되는지에 대한 것이다.
기본 스케줄러의 경우, 자체적으로 인증서를 가지고 있어 이를 기반으로 인증 인가가 가능하다.
스케줄러는 클러스터 내에서 system:kube-scheduler
라는 신원을 가지고 있다.
k get clusterrolebindings.rbac.authorization.k8s.io system:kube-scheduler -oyaml
단순히 system:kube-scheduler
란 클러스터롤을 받고 있으므로, 해당 클롤만 확인하면 스케줄러에 필요한 권한을 알 수 있다.
해당 클롤은 딱히 aggreagation이 없어서 그냥 마음 편하게 이용하면 될 듯하다.
여기에 추가적으로 스케줄러의 기본 양식 파일을 참고하여 다음과 같이 양식 파일을 짰다.
apiVersion: v1
kind: Pod
metadata:
labels:
component: kube-scheduler
tier: control-plane
name: custom-scheduler
namespace: kube-system
spec:
containers:
- command:
- kube-scheduler
# - --bind-address=127.0.0.1
# - --leader-elect=false
- --config=/etc/kubernetes/scheduler/scheduler-conf.yaml
- -v=5
image: registry.k8s.io/kube-scheduler:v1.32.2
livenessProbe:
failureThreshold: 8
httpGet:
host: 127.0.0.1
path: /livez
port: 10259
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 15
name: kube-scheduler
readinessProbe:
failureThreshold: 3
httpGet:
host: 127.0.0.1
path: /readyz
port: 10259
scheme: HTTPS
periodSeconds: 1
successThreshold: 1
timeoutSeconds: 15
resources:
requests:
cpu: 100m
volumeMounts:
- mountPath: /etc/kubernetes/scheduler.conf
name: kubeconfig
readOnly: true
- name: scheduler-config
mountPath: /etc/kubernetes/scheduler
terminationGracePeriodSeconds: 30
serviceAccountName: custom-scheduler
hostNetwork: true
priorityClassName: system-node-critical
securityContext:
seccompProfile:
type: RuntimeDefault
tolerations:
- effect: NoExecute
operator: Exists
volumes:
- hostPath:
path: /etc/kubernetes/scheduler.conf
type: FileOrCreate
name: kubeconfig
- name: scheduler-config
configMap:
name: scheduler-config
완벽하게 어떤 부분이 없어도 되는지 파악하지 못 해서 얼추 줄여도 된다 싶은 부분만 줄였다.
내 스케줄러는 x509 인증을 하지 않을 것이기에, 서비스 어카운트를 따로 지정해주었다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: custom-scheduler
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: system:custom-scheduler
rules:
- apiGroups:
- ""
- events.k8s.io
resources:
- events
verbs:
- create
- patch
- update
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- create
- apiGroups:
- coordination.k8s.io
resourceNames:
- custom-scheduler
resources:
- leases
verbs:
- get
- list
- update
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leasecandidates
verbs:
- create
- delete
- deletecollection
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- pods
- configmaps
verbs:
- delete
- get
- list
- watch
- apiGroups:
- ""
resources:
- bindings
- pods/binding
verbs:
- create
- apiGroups:
- ""
resources:
- pods/status
verbs:
- patch
- update
- apiGroups:
- ""
resources:
- replicationcontrollers
- services
verbs:
- get
- list
- watch
- apiGroups:
- apps
- extensions
resources:
- replicasets
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
- statefulsets
verbs:
- get
- list
- watch
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- persistentvolumeclaims
- persistentvolumes
verbs:
- get
- list
- watch
- apiGroups:
- authentication.k8s.io
resources:
- tokenreviews
verbs:
- create
- apiGroups:
- authorization.k8s.io
resources:
- subjectaccessreviews
verbs:
- create
- apiGroups:
- storage.k8s.io
resources:
- csinodes
verbs:
- get
- list
- watch
- apiGroups:
- storage.k8s.io
resources:
- volumeattachments
- storageclasses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- apiGroups:
- storage.k8s.io
resources:
- csidrivers
verbs:
- get
- list
- watch
- apiGroups:
- storage.k8s.io
resources:
- csistoragecapacities
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:custom-scheduler
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:custom-scheduler
subjects:
- kind: ServiceAccount
name: custom-scheduler
namespace: kube-system
이건 그냥 기본 스케줄러가 가지는 RBAC 권한을 그대로 복붙해주었다.
아직 설정이 끝나지 않았는데, 바로 컨피그맵 구성이다.
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-config
namespace: kube-system
data:
scheduler-conf.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: false
profiles:
- schedulerName: custom-scheduler
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 1
pluginConfig:
- name: NodeResourcesFit
args:
apiVersion: kubescheduler.config.k8s.io/v1
kind: NodeResourcesFitArgs
scoringStrategy:
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
type: MostAllocated
설정 파일을 동적으로 구성할 수 있도록 컨피그맵으로 스케줄러 설정파일을 만들어 관리한다.
근데 스케줄러에는 컨피그 파일의 변경을 동적으로 감지하고 리로드하는 기능이 없어서 결국 다시 시작하긴 해야 한다.
대충 설정을 했는데, 이건 다음의 자료를 참고했다.[1]
는 막상 해보니 개판이다 ㅋㅋ
정적 파드로 띄워지는 것과 클러스터의 관리를 받으며 띄워지는 것에는 어느 정도 차이가 있는 것이 아닌가 하는 생각이 든다.
아니면 내가 단순하게 이름 기반으로 클롤을 찾아서 권한 세팅을 잘못한 것일 수도 있다.
어느 쪽이든, 해결 방법은 간단하다.
그냥 로그를 보고 부족하다고 찡찡대는 리소스가 뭔지 파악하고 세팅했던 권한 양식에 계속 추가해주는 식으로 세팅했다.
기본 확인
너무 대충 세팅해서 그런가 워커 노드에 배치됐지만, 일단 성공적으로 배치가 완료됐다.
제대로 동작하고 있으니, 실질적으로 이런 방법은 관리형 쿠버네티스에서도 사용할 수 있는 방법이라고 봐도 무방하겠다.
여력이 된다면 eks 환경에서도 테스트를 진행해볼 듯 싶다.
로그를 보면 많은 정보를 알 수 있다.
일단 어떤 플러그인들이 사용되는지는 물론이고 각 스케줄링 플러그인의 가중치가 어떻게 되는지도 나온다.
또한 기본으로 설정되는 플러그인들의 설정 정보도 출력된다.
재밌는 정보들이 생각보다 많다.
일단 기본 리소스 패킹 전략은 MostAllocated로 설정되어 있어서, 최대한 자원이 많이 사용되는 노드에 높은 점수를 부여한다.
기본 테스트
apiVersion: v1
kind: Pod
metadata:
name: default-schedule
spec:
containers:
- name: myapp
image: nginx:latest
resources:
requests:
# cpu: 100m
memory: 200Mi
terminationGracePeriodSeconds: 1
schedulerName: custom-scheduler
단순하게 파드를 만들고, 사용할 스케줄러를 명시해준다.
만약 탐지되지 못한 스케줄러를 사용한다면 파드는 펜딩 상태로 머물게 된다.
제대로 스케줄러가 작동한다면 위와 같이 스케줄링을 진행했다는 로그가 남는다.
다른 글에서는 점수가 어떻게 매겨졌는지도 로그가 남길래 어떻게 하는 건가 하고 봤더니, 로그 레벨을 11까지 올려서 보더라..[2]
로그가 엄청 커지는 걸 감안하고 보면 이렇게 점수도 확인할 수 있다.
스케줄러 파일 보완
기본 테스트를 진행하며 가급적 hostNetwork를 사용하는 것을 피하고자 여러 세팅을 하다가 최종적으로는 이렇게 세팅하게 됐다.
apiVersion: v1
kind: Pod
metadata:
name: custom-scheduler
namespace: kube-system
labels:
component: kube-scheduler
tier: control-plane
spec:
containers:
- command:
- kube-scheduler
- --config=/etc/kubernetes/scheduler/scheduler-conf.yaml
- --v=11
image: registry.k8s.io/kube-scheduler:v1.32.2
name: kube-scheduler
resources:
requests:
cpu: 100m
volumeMounts:
- name: scheduler-config
mountPath: /etc/kubernetes/scheduler
readOnly: true
terminationGracePeriodSeconds: 30
serviceAccountName: custom-scheduler
priorityClassName: system-node-critical
tolerations:
- effect: NoExecute
operator: Exists
volumes:
- name: scheduler-config
configMap:
name: scheduler-config
기본 스케줄러 양식은 빼기만 하고 크게 달라진 것은 없다.
디플로이먼트로 만드려다가, 디버깅을 조금 더 용이하게 하기 위해 그냥 파드로 계속 유지하기로 결정했다.
프로브는 빼지 않는 게 좋지만.. 로깅이 힘들어 잠시 제외시켰다.
apiVersion: v1
kind: ConfigMap
metadata:
name: scheduler-config
namespace: kube-system
data:
scheduler-conf.yaml: |
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler/kubeconfig
leaderElection:
leaderElect: false
profiles:
- schedulerName: custom-scheduler
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 2
disabled:
- name: NodeResourcesBalancedAllocation
- name: PodTopologySpread
pluginConfig:
- name: NodeResourcesFit
args:
apiVersion: kubescheduler.config.k8s.io/v1
kind: NodeResourcesFitArgs
scoringStrategy:
resources:
- name: cpu
weight: 1
- name: memory
weight: 10
type: MostAllocated
kubeconfig: |
apiVersion: v1
kind: Config
contexts:
- context:
cluster: kubernetes
namespace: kube-system
user: custom-scheduler
name: custom-scheduler@kubernetes
current-context: custom-scheduler@kubernetes
clusters:
- cluster:
certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
server: https://kubernetes.default.svc.cluster.local
name: kubernetes
users:
- name: custom-scheduler
user:
tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
컨피그맵을 굉장히 많이 수정했는데, kubeconfig 파일을 넣어 명확하게 파드에 지정한 서비스어카운트의 신원을 이용해 api 서버와 통신을 하게 했다.
이 파일이 있더라도 직접적으로 스케줄러의 인자로 넣으면 인식이 되지 않는다.
왜냐하면 config 인자가 들어가는 순간 config 파일에서 할 수 있는 모든 세팅의 값으로 모든 것이 덮어 씌워지기 때문이다.
config 파일에서 커넥션 관련 정보를 넣지 않으면 해당 값이 아예 없는 것으로 치부되어버리기 때문에, config 파일 속에 세팅을 해야 한다.
어려운 건 아니고, 단순하게 clientConnection.kubeconfig
필드에 kubeconfig 파일 경로를 써주면 된다.
kubeconfig에서는 유저 신원 검증을 위해 서비스 어카운트 토큰을 사용하도록 했는데 tokenFile
필드로 경로를 넣어주면 해당 파일의 토큰을 가져와 인증에 사용한다.
커스텀 리소스를 활용해 스케줄링하기
이제 본격적으로 내가 의도한 대로 스케줄링이 동작하도록 해본다.
전략
일단 스케줄러가 어떻게 동작해야 할지 먼저 정의해보자.
- custom.resource/bbb가 있는 자원에는 배치되지 않는다. - 필터링
- custorm.resource/aaa가 많이 사용된 곳에 배치되게 한다. - 스코어링
- custorm.resource/aaa가 많은 곳에 배치되게 한다. - 스코어링
간단한 정도로는 이 정도로 전략을 짜보았다.
이를 위해 현재 사용하는 노드에 먼저 커스텀 리소스를 등록해야 한다.
- 마스터 노드
- aaa 5개
- aaa 3개 사용됨
- 워커 노드 1
- aaa 10개
- 워커 노드 2
- aaa 12개
- bbb 1개
처음에는 이 상태로 시작하여 aaa를 요청하는 첫번째 파드가 마스터 노드에 배치되게 한다.
그 다음, 직접 워커노드1에 aaa를 4개 사용하는 파드를 배치하고 다시 파드를 배치한다.
이때는 마스터 노드와 워커노드1이 aaa를 똑같이 사용하므로 aaa가 많은 워커 노드 1에 파드가 배치되면 성공이다.
사전 세팅
curl --header "Content-Type: application/json-patch+json" \
--request PATCH \
--data '[{"op": "add", "path": "/status/capacity/example.com~1foo", "value": "5"}]' \
http://k8s-master:8080/api/v1/nodes/k8s-node-1/status
이런 식으로 커스텀 리소스를 노드에 넣어, 이게 최대한 많이 사용된 곳에 파드가 배치되게 할 것이다.[3]
실제 gpu라면 아마 gpu 자원을 디바이스 플러그인으로 관리하는 경우가 많을 텐데, 이걸 커스텀해서 만드는 것도 가능하다.
kubectl patch nodes worker1 --type='json' -p='[{"op": "add", "path": "/status/capacity/custom.resource~1aaa", "value":"10"}]'
curl로 못할 건 없지만, 귀찮게 인자 추가해서 넣고 싶지도 않고 kubectl에서도 patch를 지원해보니 이걸 활용해본다.
는 실패..
proxy를 임시로 세워서 하는 방법이 있긴 한데, 별로 하고 싶은 방법은 아니다.[4]
k krew install edit-status
대신 이를 지원해주는 플러그인을 찾아서 이것을 활용해본다.[5]
음.. 이걸로도 안 바뀌는 것 같다..?
kubectl patch nodes worker1 --subresource='status' --type='json' -p='[{"op": "add", "path": "/status/capacity/custom.resource~1aaa", "value":"10"}]'
뒤늦게 알았는데, 서브리소스 중 status를 바꿀 때는 이런 식으로 따로 명시를 해주어야 한다.
보통 status가 사용자의 수정에 영향을 받지 않는다는 것은 알았지만, 이렇게 해서 세팅할 수 있다는 것은 또 처음 알았다.
kubectl patch nodes master1 --subresource='status' --type='json' -p='[{"op": "add", "path": "/status/capacity/custom.resource~1aaa", "value":"5"}]'
kubectl patch nodes worker2 --subresource='status' --type='json' -p='[{"op": "add", "path": "/status/capacity/custom.resource~1aaa", "value":"12"}]'
kubectl patch nodes worker2 --subresource='status' --type='json' -p='[{"op": "add", "path": "/status/capacity/custom.resource~1bbb", "value":"1"}]'
일단 기본적인 노드 작업을 마친다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: aaa-in-master
labels:
app: aaa-in-master
spec:
selector:
matchLabels:
app: aaa-in-master
replicas: 3
template:
metadata:
labels:
app: aaa-in-master
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.kubernetes.io/control-plane
operator: Exists
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
containers:
- name: request-extra-resource
image: nginx:latest
resources:
requests:
custom.resource/aaa: 1
limits:
custom.resource/aaa: 1
마스터 노드에 사전 배치할 워크로드는 이런 식으로 세팅했다.
톨러레이션을 주어 마스터 노드에 배치될 수 있도록 했고, 노드 어피니티를 통해 컨트롤 플레인인 노드에만 배치되도록 만들었다.
제대로 배치된 것을 확인할 수 있다.
본격 실습 - 실패
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
kubeconfig: /etc/kubernetes/scheduler/kubeconfig
leaderElection:
leaderElect: false
profiles:
- schedulerName: custom-scheduler
plugins:
score:
enabled:
- name: NodeResourcesFit
weight: 10
disabled:
- name: NodeResourcesBalancedAllocation
- name: PodTopologySpread
pluginConfig:
- name: NodeResourcesFit
args:
apiVersion: kubescheduler.config.k8s.io/v1
kind: NodeResourcesFitArgs
ignoredResources:
- cpu
- memory
scoringStrategy:
resources:
- name: custom.resource/aaa
weight: 10
type: MostAllocated
이제 스케줄링 설정을 해줘야 한다.
노드 리소스의 상태를 강하게 점수 매기도록 했고, cpu와 메모리는 고려하지 않는 채로 점수를 매기도록 설정했다.
근데 한 가지 간과한 것이, 노드 필터링을 할 때 자원을 이용할 수 있을 것이라 생각했다.
그러나 기본 플러그인 중에서는 이러한 기능을 제공하는 것이 없어, 이걸 고려하게 하려면 커스텀 스케줄러를 만드는 정도가 아니라 스케줄링 플러그인까지 개발해야 한다...
당장 할 시간은 조금 부족해서 그냥 노드에 라벨을 붙이고 파드 스펙에 해당 노드를 피하도록만 세팅했다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: aaa-pod
labels:
app: aaa-pod
spec:
selector:
matchLabels:
app: aaa-pod
replicas: 1
template:
metadata:
labels:
app: aaa-pod
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: no-schedule
operator: DoesNotExist
tolerations:
- key: "node-role.kubernetes.io/control-plane"
operator: "Exists"
containers:
- name: aaa-pod
image: nginx:latest
resources:
requests:
custom.resource/aaa: 1
limits:
custom.resource/aaa: 1
schedulerName: custom-scheduler
시험할 워크로드는 이렇게 생겼다.
말했듯이 워커 노드 2에는 배치되지 않도록 노드 어피니티에 워커 노드 2만 가지고 있는 라벨을 넣어주었다.
두번째로 배포를 할 때는 단순하게 이 친구를 스케일링 해주면 된다.
첫번째 파드는 압도적인 점수 차로 마스터 노드에 배치됐다.
그 다음, 워커 노드 1에 대충 4개를 활용하는 파드를 하나 띄우고 스케일링을 진행했다.
예상치 못한 상황.. 근소하게 차이가 좁혀질 거라 생각했건만, 생각보다 간극이 크게 줄지 않았다.
MostAllocated 전략은 노드에 자원이 채워진 정도도 고려하는 것으로 보인다.
RequestedToCapacityratio만 그런 줄 알았는데..
본격 실습 - 재도전
처음 생각한 전략의 의미를 다시금 생각해봤다.
일단 실질적으로 현재 기본으로 주어진 플러그인을 커스텀하는 식의 설정에서는 사용할 수 있는 플러그인이 NodeResourceFit
밖에 없다.
이 플러그인은 자원이 많이 할당된 곳, 혹은 적게 할당된 곳에 점수를 높게 주는 식으로 밖에는 세팅을 하지 못한다.
이 중간 사이에서 비율을 세팅하고자 할 때 RequestedToCapacityRatio를 활용할 수 있기는 하다.
그러나, 결국 요지는 하나의 선형 함수로만 사용하는 꼴이고, 이 플러그인 만으로는 내가 생각한 두가지 스코어링 전략을 만족시킬 수 없다.
그렇다면 이런 방식을 생각해볼 수 있다.
일단 처음 스코어링 기준인 자원이 많이 쓰인 곳에 파드 배치하기, 이것은 기존처럼 NodeResourceFit
플러그인으로 적용할 만한 기준이다.
그 이후 두번째 기준인 자원이 많은 곳에 배치하기는 조금 의미를 바꿔, 고르게 자원을 배치하려는 것으로 생각해볼 수 있다.
그렇다면 NodeResourceBalacedAllocation
플러그인을 사용할 수 있지 않을까?
기본적으로는 노드를 채우는 것에 중점을 두도록 점수가 부여되나, 채우다가 점차 고르게 배치하는 쪽으로 방향을 틀게 되도록 가중치를 배치하는 것이다.
직접 하다보니 왜 저번 KKCD에서 발표자 분이 커스텀 스케줄러를 개발하셨는지 확 체감이 된다 ㅋㅋ
처음에는 역시 마스터 노드에 배치되도록 점수가 부여된다.
근데 조금 싸한 것이 밸런스쪽 점수가 그대로다..
세팅을 하고 두번째 파드를 배치했는데, 이번에도 밸런스에 대한 점수는 그대로이다.
그렇다면 아무래도 이건 내가 플러그인의 의미를 잘못 해석한 것일 가능성이 높다.
이건 노드 간 자원 분배를 신경쓰는 플러그인이 아니라, 어떤 노드에 배치했을 때 파드가 요구한 자원들이 밸런스있게 소모되는지를 따지는 플러그인인 것이다.
그렇다면 내가 기대한 전략에 사용할 수는 없다.
여기에서 한가지 더 생각이 든 것은, 파드 어피니티를 활용하는 전략이다.
조금씩 가중치를 수정해가면서 하면 할 수 있겠다는 생각은 드는데, 이것까지 하기에는 시간이 조금 부족해서 현재 실습은 여기에서 마무리하려고 한다.
아쉽지만 커스텀 리소스를 등록하는 방법, 그리고 커스텀 스케줄러를 만드는 방법에 대한 지식과 경험을 쌓은 것만으로 만족해야 할 듯하다.
사실 처음에 구상한 전략이 현실적인 운영 케이스에 맞는가에 대한 고려도 해봐야한다.
가능한 자원이 많이 사용되고 있는 곳에 꽉꽉 파드를 배치하는 것은 충분히 가능한 전략이다.
특히나 gpu처럼 단위가 정수로 나눠떨어지는 자원이라면 그렇다.
그러나 그러면서도 개수가 비슷해지면 자원이 많은 곳으로 배치되도록 한다?
어감이 조금 이상하다고 느껴진다.
차라리 이런 전략이면 조금 말이 되는 것 같다.
기본적으로는 자원이 많은 곳으로 파드를 배치한다.
이때부터 자원이 많이 사용되는 곳에 점수를 높게 주는 것이다.
이 전략은 충분히 말이 되는데, 이걸 처음에 생각하지 않는 것은 아니다.
다만 스케줄러의 점수가 구해지는 방식을 구체적으로 알기 전까지는 사실 어차피 스케줄러가 알아서 자원이 많은 노드를 골라줄 것이라고 생각했기에 이 전략은 그냥 MostAllocated 전략과 동치라고 생각했을 뿐이다.
그러나 막상 점수가 산정되는 것을 보니, 오히려 이런 전략은 직접적으로 스케줄러를 커스텀해야만 이룰 수 있는 전략이다.
다음에 시간이 난다면 이 전략을 집중적으로 파볼까 한다.
자원 삭제
kubectl patch nodes worker2 --subresource='status' --type='json' -p='[{"op": "remove", "path": "/status/capacity/custom.resource~1bbb"}]'
kubectl patch nodes worker2 --subresource='status' --type='json' -p='[{"op": "remove", "path": "/status/capacity/custom.resource~1aaa"}]'
kubectl patch nodes worker1 --subresource='status' --type='json' -p='[{"op": "remove", "path": "/status/capacity/custom.resource~1aaa"}]'
kubectl patch nodes master1 --subresource='status' --type='json' -p='[{"op": "remove", "path": "/status/capacity/custom.resource~1aaa"}]'
실습이 끝나면 만들었던 커스텀 자원은 싹 없애주자.
파게이트
테라폼 세팅
막상 해보니 이렇게 coredns가 계속 동작하지 않는다.
이 어노테이션이 달리는 것만으로는 부족한 것일수도 있다.
kubectl patch deployment coredns -n kube-system --type json -p='[{"op": "remove", "path": "/spec/template/metadata/annotations/eks.amazonaws.com~1compute-type"}]'
이게 제대로 반영이 안 되는 것 같아서 수정.
이번에는 이미지 풀이 안 된다.
resource "aws_route" "database_nat_gateway" {
count = var.use_nat ? 1 : 0
route_table_id = aws_route_table.private_route_table.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_gateway[0].id
timeouts {
create = "5m"
}
}
다행이 문제는 단순하게 nat 게이트웨이를 뚫어놓고 라우팅 테이블 구성을 안한 문제였다.
기본적인 구성은 성공적으로 이뤄진 것으로 보인다.
vpc cni와 kube proxy는 데몬셋이라 들어가지 않는 모습이 보인다.
이런 이슈로 인해 제대로 생성되지 않는다.
apiVersion: v1
kind: Namespace
metadata:
labels:
aws-observability: enabled
name: aws-observability
spec: {}
---
kind: ConfigMap
apiVersion: v1
metadata:
name: aws-logging
namespace: aws-observability
data:
flb_log_cw: "false" # Set to true to ship Fluent Bit process logs to CloudWatch.
filters.conf: |
[FILTER]
Name parser
Match *
Key_name log
Parser crio
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
Keep_Log Off
Buffer_Size 0
Kube_Meta_Cache_TTL 300s
output.conf: |
[OUTPUT]
Name cloudwatch_logs
Match kube.*
region region-code
log_group_name my-logs
log_stream_prefix from-fluent-bit-
log_retention_days 60
auto_create_group true
parsers.conf: |
[PARSER]
Name crio
Format Regex
Regex ^(?<time>[^ ]+) (?<stream>stdout|stderr) (?<logtag>P|F) (?<log>.*)$
Time_Key time
Time_Format %Y-%m-%dT%H:%M:%S.%L%z
이 케이스는 이전에 겪어본 적이 있어 쉽게 해결할 수 있었다.
성공적으로 배포된 것이 확인된다.
애드온 설치
파게이트 환경에서 다양한 애드온들을 설치해보려고 한다.
구체적으로 설치하려는 것은 다음과 같다.
- alb controller
- external dns
- kube ops view
external dns에서 이러한 에러가 발생했다.
매칭되는 엔드포인트가 없다.
이 친구가 문제다.
근데 엔드포인트는 잘 있다.
어쩌면 파게이트가 실행되는데 시간이 오래 걸려 발생한 문제일지도 모른다.
평소와 다르게 alb 컨트롤러의 롤 설정이 잘 안 된 것으로 보인다.
크흠...이것저것 리팩토링하다 실수를..
수정하고 컨트롤러에 뮤테이팅이 적절하게 들어가도록 재기동시켰다.
이제 제대로 볼 수 있게 됐다.
기본 확인
eks 콘솔에서는 compute 탭에 들어가면 프로필들을 확인할 수 있다.
기본적으로 세팅될 때 파게이트는 두 가지 정책을 부여 받는다.
먼저 기본적으로 클러스터 네트워크를 위한 CNI 정책이 하나 붙는다.
파게이트가 띄워진 인스턴스에 CNI 프로세스가 띄워져있을 것이라 추측해볼 수 있다.
또한 ECR에 접근하여 이미지를 받아오는 정책도 받는데, 말 그대로 ECR에서 이미지를 가져올 때 사용될 것으로 보인다.
근데 이름이 PodExcecution인 것을 보면, 파게이트의 컨테이너 환경을 구축하기 위한 다른 소프트웨어를 받기 위함일 수도 있다.
노드 확인
k get nodes
파게이트에서 파드가 하나의 노드에 배치되기 때문에, 노드가 매우 많아지는 것을 볼 수 있다.
정직하게 하나의 파드만 배치된다..
k get nodes fargate-ip-192-168-12-142.ap-northeast-2.compute.internal -oyaml
아무 노드나 뜯어서 살펴보면 위와 같이 테인트가 걸리는 것을 확인할 수 있다.
한 파게이트에 두 파드 띄우기를 실험해보고 싶었는데, 애초에 파드 개수에 제한이 걸려 있어 그건 불가능하다.
아무런 자원 요청 정보 없이 띄웠을 때 생기는 노드는 위와 같이 cpu 2개, 메모리 4기가로 세팅된다.
노드의 정보는 ec2 인스턴스가 아니기에 인스턴스로는 확인할 수 없지만, 그럼에도 서브넷에 연결되는 네트워크 인터페이스 정보든 확인할 수 있다.
보안그룹은 클러스터의 기본 보안그룹이 세팅된다.
요청자 ID, 인스턴스 소유자가 전부 다르다는 것도 하나의 특징이다.
파드 확인
각 파드는 해당 노드의 IP와 정확하게 일치하는 IP를 가지게 된다.
파드의 스펙 정보를 보면, 스케줄러가 fargate-scheduler로 바뀐 것을 확인할 수 있다.
또한 중요도가 system-node-critical로 설정된다.
이 값은 가장 높은 값이라 이걸 넘길 수 없고, 어떤 파드도 이 파드를 축출시키며 선점할 수 없게 된다.
k get mutatingwebhookconfigurations.admissionregistration.k8s.io
스케줄러와 각종 설정을 넣는 것은 뮤테이팅 웹훅을 통해 이뤄진다.
마음대로 파게이트에 파드를 띄우는 시도
apiVersion: v1
kind: Pod
metadata:
name: debug
namespace: default
spec:
containers:
- image: nicolaka/netshoot
name: debug
command:
- sh
- -c
- "tail -f /dev/null"
간단하게 파드를 만드려고 하면, default 네임스페이스를 배치할 노드가 없어서 아무런 일도 발생하지 않는다.
파게이트 프로필에서 default 네임스페이스에 대한 설정은 하지 않았기에 기본 스케줄러가 동작한다.
tolerations:
- key: "eks.amazonaws.com/compute-type"
operator: "Exists"
톨러레이션 이슈로 보이더라도, 어차피 위에서 보았듯 각 노드는 하나의 파드만 실행할 수 있기에 소용 없다.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: custom-critical
description: 해해햏
preemptionPolicy: PreemptLowerPriority
value: 2000001001
아예 파게이트를 선점하게 하고 싶어도 불가능하다.
labels:
eks.amazonaws.com/fargate-profile: fargate
...
schedulerName: fargate-scheduler
아예 파게이트 스케줄러를 사용하도록 해본다.
결국엔 소용없다.
샘플 앱 배포
k create ns fargate
파게이트 네임스페이스를 사용하기로 했으니 네임스페이스를 만든다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test
namespace: fargate
annotations :
"alb.ingress.kubernetes.io/scheme" : "internet-facing"
"alb.ingress.kubernetes.io/success-codes" : "200-399"
"alb.ingress.kubernetes.io/target-type" : "ip"
spec:
ingressClassName: alb
rules:
- host: test.zerotay.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test
port:
number: 80
---
apiVersion: v1
kind: Service
metadata:
name: test
namespace: fargate
spec:
selector:
app: test
type: ClusterIP
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
namespace: fargate
labels:
app: test
spec:
selector:
matchLabels:
app: test
replicas: 2
template:
metadata:
labels:
app: test
spec:
containers:
- name: test
image: public.ecr.aws/l6m2t8p7/docker-2048:latest
resources:
limits:
cpu: 100m
memory: 250Mi
ports:
- containerPort: 80
name: test
기본적인 워크로드를 배포해보자.
실습을 위해 빨리 끝냈다!!
배포되는데 시간이 제법 걸리긴 하지만, 결국 원하는대로 워크로드가 생기는 것을 확인할 수 있다.
잡 띄우기
apiVersion: batch/v1
kind: Job
metadata:
name: busybox
namespace: fargate
spec:
template:
spec:
containers:
- name: busybox
image: busybox
command: ["/bin/sh", "-c", "sleep 10"]
restartPolicy: Never
파게이트를 사용하기 가장 적합한 워크로드는 일시적으로만 사용하는 잡일 것이다.
주의점은 잡은 자신이 사라지기 전까지는 파드를 계속 남겨둔다는 점이다.
그래서 위의 파드도 성공으로 종료된 이후 계속 살아있는 것을 볼 수 있다.
이럴 경우 파드가 사라지지 않았기 때문에 파게이트의 노드 역시 계속 남아있게 된다.
그래서 파게이트에서 잡을 사용할 때는 ttlSecondsAfterFinished
을 걸어 잡이 끝난 후 알아서 삭제되도록 하거나, Cronjob이나 KEDA의 ScaledJob과 같은 잡 상위 컨트롤러를 활용하는 것이 좋다.
`
파이어크래커 탈취
오토모드
오토모드는 EKS에서 자체적으로 데이터플레인에 필요할 만한 핵심적인 세팅들을 직접 관리해주는 모드이다.
기존에 클러스터를 운영할 때는 VPC CNI, ALB Controller, AWS EBS CSI Driver, 등 직접적으로 설치를 하고 관리해야 하는 애드온이 굉장히 많았다.
여기에 클러스터 오토스케일링이나 인스턴스의 버전 관리 등의 요소도 전부 사용자가 관리해야 하는 영역이었다.
그러나 이런 부분들은 실질적으로 eks를 사용하는데 있어 안 쓰는 케이스도 전무하다 싶기도 하고, 사용자가 운영하면서 실수가 날 수 있는 부분들이 종종 있었다.
이에 AWS에서 이런 부분까지도 통합적으로 관리하는 솔루션으로서 오토모드를 출시하게 된 것이다.
보다시피 컨트롤 플레인 관리 영역 뿐만 아니라, 위에서 언급한 다양한 부분들이 전부 aws의 관리 영역으로 들어간 채로 활용하는 것이 오토모드이다.
참고로 aws의 관리 영역이 늘어나는 만큼, 가격은 조금 더 비싸진다.
(가시다님 공유 자료)
오토모드의 노드에는 이미 다양한 것들이 기본 프로세스로 돌아간다.
흔히 클러스터를 운영할 때 파드로 띄우던 것들을 그냥 기본 프로세스로 동작을 시켜서 클러스터를 운영하는 사용자 입장에서는 각각을 신경 쓸 필요가 없게 된다.
기능
그래서 어떤 기능들이 구체적으로 있을까?
인스턴스
일단 사용되는 ami는 Bottlerocket으로, 기본적으로 컨테이너 환경에서 보안과 성능을 최적화한 인스턴스를 사용하게 된다.
(이것 때문에 노드 디버깅이 조금 어려워지는 효과가 있다..)
구성품
그리고 다음의 것들이 추가 세팅되어 aws에서 관리된다.
이것들은 전부 노드의 기본 프로세스로서 동작하기에 클러스터 차원에서 확인할 수는 없다.
- 클러스터 오토스케일링
- 카펜터가 사용되는데 스펙이 조금 달라지기에 마이그레이션 시 주의가 필요하다.
- 네트워크
- VPC CNI를 무조건 활용한다.
- ALB Controller가 활성화된다.
- Core DNS를 사용한다.(근데 이걸 안 쓰는 곳이 있기는 하냐? 거의 상상 속의 기린 아님?)
- 스토리지
- AWS EBS CSI Driver가 활성화된다.
- 보안
- pod identity agent가 활성화된다.
그러고 보니 모니터링 관련은 없네..?
업그레이드
애드온 버전, 인스턴스가 알아서 적당히 업그레이드된다.
클러스터 버전에 맞춰 애드온이 알아서 업데이트되니 이리 편할 수가 없다.
다만 노드가 업그레이드 된다는 것은 달리 말하자면 노드가 교체된다는 것이다.
노드의 교체에 따른 운영 장애 최소화를 위해 PDB 같은 것을 설정해두는 것이 좋다.
오토모드에서는 노드의 만료 기간을 설정해두어 적절한 시간이 지나면 새로운 노드가 뜨고 기존 노드가 없어진다.
유의사항
기본적으로 오토모드에서 안 되는 것들이 있는데, 이것들을 유의할 필요가 있다.
- 파드 별 보안 그룹은 지원하지 않는다.
- VPC CNI의 커스텀 네트워킹, warm IP, 접두사 위임 관련 설정을 지원하지 않는다.
- 네트워크 폴리시의 컨트랙 커스텀을 지원하지 않는다.
마이그레이션 시
기본적으로 설정되는 애드온과 툴들의 api 그룹과 버전이 달라지기 때문에, 마이그레이션에는 주의가 필요하다.
오죽하면 새로 클러스터를 만드는 게 낫다는 말이 나오는 게 아니다.
이렇게 변화를 주어 더 번거롭게 하는 데에는, 사실 마이그레이션 상의 장애를 줄이고자 하는 목적이 크다.
기존에 카펜터를 활용하던 조직에서 갑자기 오토모드를 도입하게 되면 기존 노드들이 새로 깔린 카펜터에 의해 관리되어버리는 불상사가 발생할 수 있다.
오토 모드 도입으로 인해 기존 클러스터에 차질이 생기는 것을 막기 위해, 운영자가 명시적으로 새롭게 양식을 마이그레이션하도록 유도하는 것이다.
테라폼 세팅
cluster_compute_config = {
enabled = true
node_pools = ["general-purpose"]
}
테라폼에서 오토 모드를 세팅하는 것은 간단하다.
eks 리소스 블록에 위의 블록을 넣어주면 된다.
노드 풀은 카펜터가 동작할 때 기본으로 세팅할 노드풀을 지정한다.
여기에는 두 가지 방식이 가능한데, general-purpose가 흔히 사용하는 형태이다.
system의 경우에는 CriticalAddonsOnly
라는 테인트가 붙는다.
이를 통해 클러스터의 핵심이 되는 애드온들만이 배치되는 노드풀이 된다.
기본 확인
콘솔에서는 eks 콘솔 바로 첫 탭에 오토모드에 대한 활성화여부가 출력된다.
설정한 대로 general-purpose 노드풀이 생성된 것을 확인할 수 있다.
보다시피 아무런 애드온이 없는 상태이다!
그럼에도 coredns, proxy 등 기본으로 활성화된 기능들이 있다는 것을 노드에 들어가면 보게 될 것이다.
액세스 엔트리를 보면 노드들이 기본으로 사용할 엔트리와, 각 애드온들이 수행될 때 필요한 엔트리가 생성되는 것을 볼 수 있다.
kubectl로 클러스터 상태도 관찰해보자.
kube-system 네임스페이스에는 여러 컨트롤러 매니저에 있던 컨트롤러들마다 서비스 어카운트가 만들어지는 것을 확인할 수 있다.
k get nodepools.karpenter.sh -oyaml
기본 노드풀은 이런 식으로 생겼다.
직접 카펜터를 세팅하지 않았음에도, 오토모드에서는 알아서 카펜터가 동작하고 있음을 확인할 수 있다.
k get crd
crd를 보면 여러 crd가 이미 사전에 설치돼있는데, 이미 이전에 각종 애드온들을 설치하고 활용해봤다면 전부 낯이 익을 것이다.
nodediagnostics 보기
이 중 nodediagnostics라는 것은 처음 보는데, 이를 확인하기 위해 간단한 파드를 만들어보자.
apiVersion: v1
kind: Pod
metadata:
name: debug
namespace: default
spec:
containers:
- image: nicolaka/netshoot
name: debug
command:
- sh
- -c
- "tail -f /dev/null"
resources:
limits:
cpu: 100m
memory: 100Mi
terminationGracePeriodSeconds: 0
기본적으로 노드는 없는 상태였으나, 파드가 배포됨에 따라 카펜터가 알아서 노드를 추가해주었다.
이들은 전부 보틀로켓으로 만들어진다.
해당 노드에 매칭되는 노드클레임 역시 확인할 수 있다.
https://docs.aws.amazon.com/eks/latest/userguide/auto-get-logs.html
nodediagnostic은 직접 만들어야 하며, 이것을 사용하면 S3에 관련 정보를 저장하게 된다.
import boto3
from datetime import datetime
import argparse
region = 'ap-northeast-2'
bucket_name = 'aews-eks-zerotay'
s3_client = boto3.client('s3', region_name=region)
key = datetime.now().strftime('%y-%m-%d') + '/logs.tar.gz'
def create_bucket():
try:
s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={'LocationConstraint': region})
return True
except Exception as e:
print(e)
return False
def delete_bucket():
response = s3_client.delete_bucket( Bucket=bucket_name)
print(response)
def list_buckets():
response = s3_client.list_buckets(BucketRegion=region)
return map(lambda x: x["Name"], response["Buckets"])
def generate_presigned_url():
url = s3_client.generate_presigned_url(
ClientMethod='put_object',
Params={'Bucket': bucket_name, 'Key': key},
ExpiresIn=1000
)
return url
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="put subcommand")
parser.add_argument("--delete", type=bool, default=False, help="delete the bucket or not")
parser.add_argument("--get_list_only", type=bool, default=False, help="list buckets only")
args = parser.parse_args()
if args.delete:
delete_bucket()
else:
bucket_lst = list_buckets()
print('Current buckects ::')
print(list(bucket_lst))
if args.get_list_only:
exit(0)
if bucket_name not in bucket_lst:
print(f' Bucket {bucket_name} needs to be created...')
if not create_bucket():
raise Exception
bucket_lst = list_buckets()
print('Current buckects ::')
print(list(bucket_lst))
url = generate_presigned_url()
print('Presigned URL for putting objects')
print(url)
간단하게 aws cli로도 s3는 만들 수 있는데, 찾아보니 put이 가능한 presigned url을 만드는 방법이 없다.
그래서 어쩔 수 없이 파이썬으로 대충 코드를 만들었다.
이런 식으로 presigned URL이 나오면 성공이다.
이제 이 값을 이용해 직접 nodediagnostic 오브젝를 만들어주면 된다.
apiVersion: eks.amazonaws.com/v1alpha1
kind: NodeDiagnostic
metadata:
name: 노드 이름
spec:
logCapture:
destination: 만들어진 url
제대로 만들었다고 생각했는데, 왜인지 fatal error가 계속 나왔다.
시도했던 방법은 다음과 같다.
- 혹시나? 해서 클라우드 쉘에서 파일 실행
- vpc에 액세스포인트 생성
- 퍼블릭 액세스 권한 열어두기
- put object 관련 허용 정책 설정
- 업로드한 주체에게 소유권 주기
s3 자체를 많이 사용해본 적이 없어 여러 방면으로 시도해봤으나, 전부 실패했다.
정확하진 않으나, 추측으로는 모니터링 정보를 업로드하는 주체는 해당 인스턴스인 것으로 보인다.
노드의 node monitoring agent가 동작하기 때문에 이게 가능하다고 언급됐기 때문이다.[6][7]
그래서 생각에는 노드가 S3에 접근할 권한이 없는 것이 아닐까 생각한다.
관련한 자료도 일체 없고, 정확히 왜 실패했는지에 대한 정보를 알려주지 않기에 이대로는 제대로 테스트를 진행할 수 없다고 판단하여 nodediagnostic은 여기까지만 하고 포기했다.
debug 컨테이너로 노드 뜯어보기
kubectl debug node/i-009b014f5df4b636c -it --profile=sysadmin --image=public.ecr.aws/amazonlinux/amazonlinux:2023
노드의 정보를 보는데 저 방법만 있는 것은 아니다.
kubectl debug는 임시 컨테이너를 만들어준다는 것은 알고 있었지만, 노드에 대해 사용하면 호스트의 파일시스템을 사용하는 컨테이너가 생긴다는 것은 처음 알았다.
아무튼 이렇게 현재 노드의 상태를 이걸로도 볼 수 있다.
df를 쳐보면, 실제 노드의 파일시스템은 /host
에 들어있는 것을 알 수 있다.
그래서 해당 디렉토리로 들어가면 실제 노드의 파일시스템을 볼 수 있게 된다.
기본적으로는 호스트의 파일시스템을 볼 수 있다고 하나, 아직 모든 호스트의 네임스페이스를 사용하고 있는 것은 아니다.
이를 위해서는 nsenter
명령어가 필요하다.
yum install -y util-linux-core net-tools
이러면 이제부터 nstenter를 쓸 수 있게 되고, 이걸 통해 루트 네임스페이스에 들어가 각종 명령어를 실행할 수 있게 된다.
nsenter -t 1 -m ip addr
nsenter -t 1 -m ps -ef
nsenter -t 1 -m ls -l /proc
nsenter -t 1 -m df -hT
일단 네트워크 인터페이스에서는 파드 아이덴티티 agent, coredns를 위한 인터페이스가 마련되는 것이 보인다.
프로세스를 보면 오토모드에서 자체적으로 지원해준다던 각종 툴들이 바이너리 파일로 존재하며 이를 그냥 기본 프로세스로 실행하는 것을 볼 수 있다.
각 프로세스가 통신을 위해 참고하는 파일은 위에서 보았듯 /host/etc/kubernetes
에 들어있다.
기본 실행되는 프로세스는 다음과 같다.
- eks 헬스체커
- kube proxy
- pod identity agent
- ebs csi driver와 칭구들
- node monitoring agent - 원래 위에서 nodediagnostic를 담당하는 애드온이다.
- vpc cni
- core dns
조금 의외라고 생각하는 것은, core dns가 모든 노드에서 실행된다는 것이다.
그런데 실제로도 core dns의 퍼포먼스 향상을 위해 로컬 dns 캐시를 설치하여 운용하는 경우도 왕왕 있으므로, 어쩌며 노드마다 설치되는 것도 그렇게 이상한 유스 케이스는 아닐 수도 있겠다는 생각도 든다.
또 유의미하게 본 포인트 중 하나는 카펜터와 alb 컨트롤러 프로세스가 존재하지 않는다는 것이다.
아무래도 클러스터를 스케일링하는 카펜터는 데이터 플레인에 상관없이 기동될 필요가 있기 때문에 추측컨대 마스터 노드에 위치해있을 것으로 보인다.
alb 컨트롤러 역시 노드의 존재 유무와 관련 없이 로드밸런서를 만들고 관리하는 작업을 해야 하므로 카펜터와 동일하게 마스터 노드에 있을 것 같다.
더 쉽지만 막힐 것 같은 방법 - hostPath
apiVersion: v1
kind: Pod
metadata:
name: hostpath
spec:
containers:
- image: nicolaka/netshoot
name: debug
command:
- sh
- -c
- "tail -f /dev/null"
securityContext:
privileged: true
volumeMounts:
- mountPath: /opt
name: host
hostNetwork: true
hostPID: true
hostIPC: true
terminationGracePeriodSeconds: 0
volumes:
- name: host
hostPath:
path: /
디버그 컨테이너로 nsenter를 하는 것도 좋지만, 그보다 훨씬 쉬운 방법도 존재한다.
특권 권한으로 시큐리티 컨텍스트를 지정하고, 그냥 노드의 모든 네임스페이스를 공유해버리면 된다.
참고로 디버그 컨테이너를 사용할 때도 미리 템플릿 작성을 해서 적용하면 같은 방식으로 가능할 것이다.
아무튼 이렇게 쉽게 노드의 정보를 파악할 수 있다.
다만 읽기만 가능하고 쓰기는 불가능한 것을 확인할 수 있다.
기본 앱 배포
기본적으로 많은 게 세팅돼있지만, 이들을 직접적으로 사용하기 위해서는 직접 오브젝트를 만들어줘야 한다.
apiVersion: eks.amazonaws.com/v1
kind: IngressClassParams
metadata:
name: alb
spec:
scheme: internet-facing
group:
name: aews-eks
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: alb
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: eks.amazonaws.com/alb
parameters:
apiGroup: eks.amazonaws.com
kind: IngressClassParams
name: alb
일단 alb controller를 쓰기 위해 인그레스 클래스를 만든다.
오토모드에서는 가급적 ingressClassParams로 각종 세팅을 하도록 권장하고 있다.
아무래도 어노테이션을 사용하는 방식은 세팅에 대한 검증을 하기가 불편하기에 도입된 건데, 거의 아무도 안 쓰다 보니까 아예 오토모드에서는 못 박아둔 것 같기도..
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test
annotations :
"alb.ingress.kubernetes.io/success-codes" : "200-399"
"alb.ingress.kubernetes.io/target-type" : "ip"
"alb.ingress.kubernetes.io/load-balancer-name" : "aews"
"alb.ingress.kubernetes.io/listen-ports" : '[{"HTTPS":443}, {"HTTP":80}]'
"alb.ingress.kubernetes.io/ssl-redirect" : "443"
spec:
ingressClassName: alb
rules:
- host: test.zerotay.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: test
port:
number: 80
---
apiVersion: v1
kind: Service
metadata:
name: test
spec:
selector:
app: test
type: ClusterIP
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
labels:
app: test
spec:
selector:
matchLabels:
app: test
replicas: 2
template:
metadata:
labels:
app: test
spec:
containers:
- name: test
image: public.ecr.aws/l6m2t8p7/docker-2048:latest
resources:
limits:
cpu: 100m
memory: 250Mi
ports:
- containerPort: 80
name: test
세팅하고 나면 사용하는 방식은 동일하다.
문제 없이 배포되는 것을 볼 수 있다.
gpu 어플리케이션 배포
이번에는 핫하다는 딥시크를 간단하게 배포해본다.[8]
사전 준비
일단 사전 준비를 해줘야 할 게 있는데, 첫번째는 허깅페이스 api 토큰을 발급 받는 것이다.
여기에서 적당한 읽기 권한을 받아서 토큰을 가져온다.[9]
k create secret generic hf-secret --from-literal hf_api_token=허깅페이스 키
해당 토큰은 바로 시크릿으로 만들어준다.
그리고 G 유형의 인스턴스를 쓸 건데, 서비스 쿼타를 미리 뚫어두지 않았다면 해당 타입의 인스턴스를 만들 수 없다.
You have requested more vCPU capacity than your current vCPU limit of 0 allows for the instance bucket that the specified instance type belongs to. Please visit http://aws.amazon.com/contact-us/ec2-request to request an adjustment to this limit.
내 경우에는 클라우드 트레일에서 뒤늦게 정보를 확인했다.
콘솔에서 서비스 쿼타를 치고, ec2에 대해 On-Demand G 인스턴스 유형에 대한 값을 올린다.
그럼 알아서 서포트 센터에 요청이 날아간다.
그아아아.. 얼마나 기다려야 하나..
허허.. 대충 4시간 뒤에 완료됐다..
이 글을 보는 당신은 꼭 미리 신청해두십쇼..
노드 풀 생성
apiVersion: eks.amazonaws.com/v1
kind: NodeClass
metadata:
name: gpu-nodeclass
spec:
role: terraform-eks-eks-auto-20250321013418751300000006
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "terraform-eks"
Name: "*Public*"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "terraform-eks"
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: gpu-nodepool
spec:
template:
spec:
nodeClassRef:
group: eks.amazonaws.com
kind: NodeClass
name: gpu-nodeclass
requirements:
- key: "eks.amazonaws.com/instance-family"
operator: In
values: ["g5"]
- key: "eks.amazonaws.com/instance-size"
operator: In
values: [ "2xlarge", "4xlarge", "8xlarge", "12xlarge", "24xlarge", "48xlarge" ]
- key: "karpenter.sh/capacity-type"
operator: In
values: ["spot", "on-demand"]
taints:
- key: "nvidia.com/gpu"
value: "true"
effect: NoSchedule
disruption:
consolidationPolicy: WhenEmpty
consolidateAfter: 30s
다음으로는 실제 워크로드의 스펙을 만족하는 노드를 만들 수 있도록 적절한 노드풀과 클래스를 만드는 것이다.
예제로 사용할 인스턴스는 40 cpu에 90 기가, gpu 4개 이상이 있어야 한다..!
워크로드 배포
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vllm-deepseek-ingress
annotations :
"alb.ingress.kubernetes.io/success-codes" : "200-399"
"alb.ingress.kubernetes.io/target-type" : "ip"
"alb.ingress.kubernetes.io/load-balancer-name" : "aews"
"alb.ingress.kubernetes.io/listen-ports" : '[{"HTTPS":443}, {"HTTP":80}]'
"alb.ingress.kubernetes.io/ssl-redirect" : "443"
spec:
ingressClassName: alb
rules:
- host: deepseek.zerotay.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vllm-service
port:
number: 80
---
apiVersion: v1
kind: Service
metadata:
name: vllm-service
spec:
selector:
app: vllm-deepseek-server
ports:
- protocol: TCP
port: 80
targetPort: 8000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-deepseek-deployment
spec:
replicas: 1
selector:
matchLabels:
app: vllm-deepseek-server
template:
metadata:
labels:
app: vllm-deepseek-server
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
automountServiceAccountToken: false
containers:
- name: inference-server
image: vllm/vllm-openai@sha256:bfb0403010061e61b692e337945fb9694baefb03b0725d62adab9cca6139ea62
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- NET_RAW
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: "40"
memory: "90Gi"
nvidia.com/gpu: 4 # 7B: 1 | 14B: 4 | 32B: 4 | 72B:
limits:
cpu: "48"
memory: "96Gi"
nvidia.com/gpu: 4 # 7B: 1 | 14B: 4 | 32B: 4 | 72B:
args:
- --model=$(MODEL_ID)
- --dtype=auto
- --enforce-eager
- --trust-remote-code
- --tensor-parallel-size=4 # Set to 8 to use all GPUs
- --gpu-memory-utilization=0.99
- --max-model-len=32768
env:
- name: MODEL_ID
value: deepseek-ai/DeepSeek-R1-Distill-Qwen-32B # 7B, 14B, 32B Works --> testing bigger sizes now
- name: PORT
value: "8000"
volumeMounts:
- mountPath: /dev/shm
name: dshm
- mountPath: /secrets
name: hf-secret-volume
readOnly: true
volumes:
- name: dshm
emptyDir:
medium: Memory
- name: hf-secret-volume
secret:
secretName: hf-secret
tolerations:
- key: nvidia.com/gpu
value: "true"
effect: "NoSchedule"
그다지 추가적인 세팅한 것 없이 정직하게 예제의 코드를 가져왔다.
보다시피 매우 큰 스펙을 요구하므로, 따로 노드풀을 만들어준 것이다.
는 서비스 쿼타가 빨리 안 풀려서 테스트를 진행할 수가 없다.
이건 후순위로 미루도록 한다.
하이브리드 노드
스터디
하이브리드는 많이 정리는 안 하셨다고 한다.
스케줄러 제대로 공부하자.
필터링과 스코어링 단계.
이 속에서 동작하는 녀석들의 우선순위
기본스케줄러는 다른 자원을 비효율적으로 쓰게한다.
그래서 전용 스케줄러를 피료하면 만들어야 한다.
파게이트도 전용스케줄러가 있다.
기본 빈패킹 알고리즘이 least allocated.
이걸 mostallocated를 쓸 수도 잇다.
테라폼
tfenv로 파이썬 venv마냥 버전 관리를 할 수 있다.
hcl은 튜링 컴플릿?
tf state list
tf show - 이걸로 상세하게 리소스 구경할 수 있다.
파게이트
데이터 플레인 서버리스
파드 간 vm 수준 격리
내부적으로 firecracker를 쓰기 때문.
하나 하나가 vm이고, 이에 대한 스케줄링을 파이어크래퍼 오케스트레이터가 한다.
파게이트 스케줄러
보안 배리어란 게 있다.
kvm 위에, 실제 vm 아래 사이에 있다.
배리어가 jailer, virtualization 두 개 있다.
컨테이너 탈옥해봤자, 할 수 있는 게 없다.
리밋 둬봤자 의미없다.
파게이트 ownd eni가 있다.
private으로만 붙는다.
최하위에 컨테이너d
이게 vm을 먼저 기동시킨다.
그 이후 내부에 다시금 컨테이너 런타임을 띄운다.
최하위 컨테이너d가 이미지, 스냅샷을 주입시킨다.
파게이트는 서브넷에 붙은 eni가 requester, instace owner 이런ㄱ ㅔ 다다르다.
pod execution role이란 게 잇는데, 이건 aws에서 파게이트를 실행하니 이때 들어가는 롤.
로깅도 된다.
우선순위도도 들어간다.
파게이트 공식문서 정리를 해두셨으니 읽자
네임스페이스나, 라벨로 스케줄링 조건이 걸린다.
넷슛 만들기
맘대로 리퀘 리밋 설정해보기.
리퀘랑 리밋은 통일돼버린다.
250메가 알아서 추가된다. - 이게 다른 컴포넌트 때무니라면, 최소한 파드 정보로 확인되어선 안 되지 않나.
노드 정보로는 보이는게 맞겠지만 - 보니까 그게 맞다.
추가되는 노드의 정보를 보면 그렇다.
보통 노드의 스펙은 설정한 것보다 올림된다.
스케줄러는 일단 필요한 스펙을 계산한다.
그리고 결과는 어노테이션에 저장된다.
넷슈에서 네트워크 정보보기
fargate owned eni는 가시다님 표현이다.
파게이트 잡은 ttl 걸어야 complete 이후로 삭제됨
이제 오토모드
aws는 관리가 가능한 부분은 아예 고객에게 보여주지도 않는 방향으로 나아간다.
고객의 휴먼 에러를 막기 위해.
그래서 오토모드가 나온다.
동등스펙으로 조금 더 비싸다.
다른 것들 더 돌려주니까 비쌀만하긴 하다.
각종 애드온들 버전관리를 편하게.
카펜터, vpc cni, csi드라이버, 모니터링 등.
rnr
관리영역을 줄여준다.
보틀로켓 고정
보면 노드가 없다..
nodediagnostics로 조금 확인할 수 있다.
노드 액세스가 안되니 이걸로 조금이라도 열어둔 것.
관련 문서
이름 | noteType | created |
---|
참고
https://cast.ai/blog/custom-kube-scheduler-why-and-how-to-set-it-up-in-kubernetes/ ↩︎
https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ ↩︎
https://kubernetes.io/docs/tasks/administer-cluster/extended-resource-node/ ↩︎
https://docs.aws.amazon.com/eks/latest/userguide/auto-troubleshoot.html ↩︎
https://docs.aws.amazon.com/eks/latest/userguide/auto-get-logs.html ↩︎
https://github.com/aws-samples/sample-aws-eks-auto-mode/tree/main/examples/gpu ↩︎